package lexical;

import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;

import common.LogPrinter;
import common.Token;
import common.TokenType;

/**
 * 
 * @author Yunke Li Boshi Li 
 * @version 1.1
 * Thanks for Ashton
 */
public class Scanner 
{
	/**
	 * If this variable is set than the Scanner should log all source
	 * lines and the tokens as they are scanned.
	 * 
	 * Example of "if(x = 1) x := x+1;\n else y := x+2;"
	 * 
	 * 1: if(x = 1) x := x+1;
	 *    1: (IF, null)
	 *    1: (LPAREN, null)
	 *    1: (ID, 0) => "x"
	 *    1: (EQ, null)
	 *    1: (NUM, 1)
	 *    1: (RPAREN, null)
	 *    1: (ID, 0) => "x"
	 *    1: (ASSIGN, null)
	 *    1: (ID, 0) => "x"
	 *    1: (PLUS, null)
	 *    1: (NUM, 1)
	 *    1: (SEMI, null)
	 *
	 * 2: else y := x+2;
	 *    2: (ELSE, null)
	 *    2: (ID, 1) => "y"
	 *    2: (ASSIGN, null)
	 *    2: (ID, 0) => "x"
	 *    2: (PLUS, null)
	 *    2: (NUM, 2)
	 *    2: (SEMI, null)
	 *    2: (ENDFILE, null)
	 */
	
	//***************************************variables begin*********************************************************************************
	//component
	private HashMap<String, Token> hashtable = new HashMap<String, Token>();
	private int index = 0;
	private ArrayList<String> spellingTable = null;
	//DFA Buffers
	private int numDel = 0;																		//the number of delimiters
	private String lexemeBuffer = "";															//record lexeme when recognize number or 'Word' token
	private int readCharacter = -2;																//the next character, since -1 represent the end of file
	private int currentState = 0;																	//current state
	private boolean hasRetracted = false;													//if it is true, then won't get next character, just use the last.
	//Print Buffers
	private Reader reader = null;																//the file reader
	private LogPrinter logPrinter = null;														//the writer of log
	private PrintStream errPrinter = null;														//the writer of err
	private String inputBuffer = "";																//record the strings of input file that haven't printed 
	private int lineNum = 1;																		//the current line
	private PrintStream tknPrinter = null;

	//*******************************************public functions begin*************************************************************************
	
	public Scanner(ArrayList<String> spellingTable, LogPrinter log, PrintStream errPrinter, PrintStream tknPrinter){
		this.spellingTable = spellingTable;
		this.logPrinter = log;
		this.errPrinter = errPrinter;
		this.tknPrinter = tknPrinter;
		initTable();
	}
	
	/**
	 * Reset the Scanner to the beginning of the provided Reader. If the
	 * reader is empty then the Scanner should return and ENDFILE token.
	 * 
	 * @param reader provides the source of ASCI characters.
	 */
	public void setReader(Reader reader){
		this.reader = reader;
	}

	/**
	 * @return the next token by consuming ASCI characters from the Reader
	 * @throws IOException Can not read the input file
	 * @throws Exception		The number has over the maxValue of Spelling Table.
	 */
	public Token nextToken() throws Exception{
		Token t = DFAAnalysis();
		String lexeme = "";
		if( ( t.getType()==TokenType.ID ) || ( t.getType()==TokenType.ERROR ) ){
			lexeme = spellingTable.get( t.getValue() );
			tknPrinter.println( String.format("%d:(%s,%s)=>%s", lineNum, t.getType() , t.getValue(), lexeme ));
			logPrinter.printTrace( String.format("(%s,%s)=>%s", t.getType() , t.getValue(), lexeme), lineNum );
		}else{
			tknPrinter.println( String.format("%d:(%s,null)", lineNum, t.getType()));
			logPrinter.printTrace( String.format("(%s,null)", t.getType()), lineNum );
		}
		if( t.getType()==TokenType.ERROR ){
			errPrinter.println("Lexical->Error Token:"+t.getValue()+" =>"+spellingTable.get(t.getValue()));
		}		
		return t;
	}
	
	/**
	 * @return current line number of input file
	 */
	public int getLineNum(){
		return lineNum;
	}
	
	/**
	 * print last input code to log
	 */
	public void printLastCode(){
		logPrinter.printInputCode(inputBuffer, lineNum);
	}
	
	//**********************************************private functions begin*****************************************************************	
	
	private Token getWordToken(String wordToken) throws Exception{
		boolean found = hashtable.containsKey(wordToken);			//found represent if hashtable can find string wordToken in its Keys.
		if( found ){
			return hashtable.get(wordToken);
		} else{
			Token id = new Token( TokenType.ID , index);
			spellingTable.add(wordToken);
			hashtable.put(wordToken, id);
			if(index==Long.MAX_VALUE){
				throw new Exception("The number has over the maxValue of Spelling Table.");
			}else{
				index++;	
			}
			return id;
		}
	} 
	
	private Token getErrorToken(String errorToken) throws Exception{
		boolean found = hashtable.containsKey(errorToken);			//found represent if hashtable can find string wordToken in its Keys.
		if( found ){
			return hashtable.get(errorToken);
		} else{
			Token id = new Token( TokenType.ERROR , index);
			spellingTable.add(errorToken);
			hashtable.put(errorToken, id);
			if(index==Long.MAX_VALUE){
				throw new Exception("The number has over the maxValue of Spelling Table.");
			}else{
				index++;	
			}
			return id;
		}
	} 
	
	private void initTable(){
		//first, add all the keywords to the hashtable
		hashtable.put("and",  new Token(TokenType.AND, null ));
		hashtable.put("bool",  new Token( TokenType.BOOL , null ));
		hashtable.put("branch",  new Token(TokenType.BRANCH, null ));
		hashtable.put("case",  new Token( TokenType.CASE , null ));
		hashtable.put("continue",  new Token( TokenType.CONTINUE , null ));
		hashtable.put("default",  new Token( TokenType.DEFAULT , null ));
		hashtable.put("else",  new Token( TokenType.ELSE , null ));
		hashtable.put("end",  new Token(TokenType.END,null ));
		hashtable.put("exit",  new Token( TokenType.EXIT , null ));
		hashtable.put("if",  new Token( TokenType.IF , null ));
		hashtable.put("int",  new Token( TokenType.INT , null ));
		hashtable.put("loop",  new Token( TokenType.LOOP ,null ));
		hashtable.put("mod",  new Token( TokenType.MOD , null ));
		hashtable.put("or",  new Token( TokenType.OR , null ));
		hashtable.put("ref",  new Token( TokenType.REF , null ));
		hashtable.put("return",  new Token( TokenType.RETURN , null ));
		hashtable.put("void",  new Token( TokenType.VOID , null ));		
		hashtable.put("not", new Token( TokenType.NOT , null ));
		//second,add Boolean to the hashtable
		hashtable.put("true", new Token( TokenType.BLIT ,1));
		hashtable.put("false", new Token(TokenType.BLIT, 0));
	}	
	
	/**
	 * simulate a pointer retract
	 * if call this method, you will get the same character when you call nextChar() next time
	 */
	private void pointerRetract() {
		if( readCharacter!=10){
			//This is not a requirement of DFA, is a requirement of recording line number. 
			//To protect read '\n' twice when retract on '\n' 
			hasRetracted = true;
		}		
	}
	
	/**
	 * @return next character in file if not retracted. Else return the same character
	 * @throws IOException
	 */
	private int nextChar() throws IOException{
		//return next character in file if not retracted. Else return the same character
		if( hasRetracted==false ){
			int character = reader.read();
			if( !isInvisible(character) ){
				//if it is invisible, ignore it in the source code line of log
				inputBuffer+=(char)character;
			}			
			return character;
		}else{
			hasRetracted=false;
			//nextCharacter has been changed, so we can just return it
			return readCharacter;
		}		
	}
	
	private boolean isDigit(char c){
		if( c>='0' && c<='9' ){
			return true;
		}else{
			return false;
		}
	}

	private boolean isLetter(char c){
		if( (c>='A' && c<='Z') || ( c>='a'&&c<='z' )  ){
			return true;
		}else{
			return false;
		}
	}
	
	private boolean isWhiteSpace( int c ){
		if( c==10 || c==32 || c==9 ){
			return true;
		}else{
			return false;
		}
	}
	
	private boolean isInvisible( int c ){
		if( ( c<=32 && !isWhiteSpace(c) )||(c>126) ){
			return true;
		}else{
			return false;
		}
	}
	
	/**
	 * 
	 * @return Token which the DFA analyzed
	 * @throws IOException Can not read the input file
	 * @throws Exception		The number has over the maxValue of Spelling Table.
	 */
	private Token DFAAnalysis() throws Exception{
				//back to state 0
				currentState = 0;
				while(true){
					//read the next character
					try {
						/*print if last time meet a '\n'
						 * the reason why it is here is as follows:
						 * 1.We can't check if the read character is '\n' after the DFA analyze it. Because the DFA meet '\n', 
						 * 	  then it will return token immediately, you hardly have time to check it.
						 * 2.In fact, it has way to check it after DFA analyze. But you need to check it each time before return Token,
						 * 	  It's a bad structure.
						 * 3.We can't check it before DFA analyze, because if you plus the line number, the last token before '\n' will be 
						 * 	  recognized in next line.
						 * So we just can check it in the next time the method called and before get next character. That time readCharacter
						 * is still the character last time, we can check it. In fact, you can think it is a way to check it after DFA analyze.
						 *
						 *it is similar as ENDFILE. Please see condition 3
						 */
						if( readCharacter == 10 ){
							logPrinter.printInputCode(inputBuffer, lineNum);
							inputBuffer="";
							lineNum++;
						}else if( readCharacter==-1 ){
							//end file need to be print by parser, since when return endfile, 
							//parser won't get another token from scanner, and match endfile info need to appear before user code 
							if( currentState==34 ){
								errPrinter.println("unexpected end of file");
							}
							return new Token(TokenType.ENDFILE, null );
						}
						readCharacter = nextChar();
					}
					catch (IOException e) {
						throw new IOException("Can not read the input file");
					}
					
					if( isInvisible( readCharacter )&&( readCharacter!=-1) ) {
						//if meet invisible character,  ignore it. But except ENDFILE 
						//because it needs to be analyzed by DFA
						continue;
					}else{
						//else use DFA to analyze read character and get Token
						char nextC = (char)readCharacter;
												
						switch( currentState  ){
						case 0:
							switch(nextC){
							case '+' : return new Token( TokenType.PLUS		,null );		//arrived state 1, then return Token
							case '=' : return new Token (TokenType.EQ			,null );		//arrived state 2, then return Token
							case ';'  : return new Token( TokenType.SEMI		,null );		//arrived state 3, then return Token
							case ','  : return new Token( TokenType.COMMA	,null );		//arrived state 4, then return Token
							case '('  : return new Token( TokenType.LPAREN	,null );		//arrived state 5, then return Token
							case ')'  : return new Token( TokenType.RPAREN	,null );		//arrived state 6, then return Token
							case '['  : return new Token( TokenType.LSQR		,null );		//arrived state 7, then return Token
							case ']'  : return new Token( TokenType.RSQR		,null );		//arrived state 8, then return Token
							case '{'  : return new Token( TokenType.LCRLY	,null );		//arrived state 9, then return Token
							case '}'  : return new Token( TokenType.RCRLY	,null );		//arrived state 10, then return Token
							case '&'	: currentState = 11; break;																//arrived state 11
							case '|'  : currentState = 14;	 break;
							case '<'	: currentState = 17; break;
							case '>'	: currentState = 20; break;
							case ':'	: currentState = 23; break;
							case '-'	: currentState = 26; break;
							case '*'	: return new Token( TokenType.MULT		,null );		//arrived state 30, then return Token
							case '/'	: currentState = 31; break;
							default:
								if( isLetter(nextC) ){
									lexemeBuffer = String.valueOf(nextC);
									currentState = 37;
								}else if( isDigit(nextC)){
									lexemeBuffer = String.valueOf(nextC);
									currentState = 39;
								}else if( (!isInvisible( readCharacter ))&&(!isWhiteSpace( readCharacter )) ){
									return getErrorToken( String.valueOf( nextC ) );
								}
							}
							break;
						case 11: //when the first character is '&'
							switch(nextC){
							case '&': return new Token( TokenType.ANDTHEN	, null );		//arrived state 12
							default:																											//arrived state 13
								pointerRetract();
								return getErrorToken("&");
							}
						case 14:
							switch(nextC){
							case '|': return new Token( TokenType.ORELSE		, null );		//arrived state 15
							default:																											//arrived state 16
								pointerRetract();
								return getErrorToken("|");
							}
						case 17:
							switch(nextC){
							case '=' : return new Token( TokenType.LTEQ			, null );		//arrived state 18
							default:																											//arrived state 19
								pointerRetract();																
								return new Token( TokenType.LT , null );
							}
						case 20:
							switch(nextC){
							case '=' : return new Token( TokenType.GTEQ			, null );		//arrived state 21
							default:																											//arrived state 22
								pointerRetract();																
								return new Token( TokenType.GT , null );
							}						
						case 23:
							switch(nextC){
							case '=' : return new Token( TokenType.ASSIGN			, null );		//arrived state 24
							default:																												//arrived state 25
								pointerRetract();																
								return new Token( TokenType.COLON , null );						  
							}
						case 26:
							switch(nextC){
							case '-' : currentState = 27; break;																		//arrived state 27
							default:																												//arrived state 28
								pointerRetract();																
								return new Token( TokenType.MINUS , null );
							}
						case 27:
							switch(nextC){
							case '\n' : currentState = 0; break;																		
							default:		break;																								//still stay at state 27
							}
							break;
						case 31:
							switch(nextC){
							case '=' : return new Token( TokenType.NEQ , null );					//arrived state 32
							case '*'	: currentState = 34; numDel = 1 ;break;
							default:	
								pointerRetract();
								return new Token( TokenType.DIV , null );								//arrived state 33
							}
							break;
						case 34:
							//if numDel = 0 which means there is the end of the delimiters, go to state 0
							if( numDel == 0 ){
								currentState = 0;
								break;
							}
							switch(nextC){
							case '/' : currentState = 36; break;					//arrived state 32
							case '*'	: currentState = 35;break;
							default:		break;
							}
							break;
						case 35:
							switch(nextC){
							case '/' : currentState =  34; numDel--; break;					//arrived state 32
							default:	 currentState =  34; break;
							}
							break;
						case 36:
							switch(nextC){
							case '*' : currentState =  34; numDel++; break;					//arrived state 32
							default:	 currentState =  34; break;
							}
							break;
						case 37:
							if( isLetter(nextC) || nextC=='_' || isDigit(nextC) || nextC == '$' ){
								lexemeBuffer+=nextC;
							} else {
								pointerRetract();
								return getWordToken(lexemeBuffer);
							}
							break;
						case 39:
							if ( isDigit(nextC) ){
								lexemeBuffer+=nextC;
							}	else {
								pointerRetract();
								return new Token(TokenType.NUM , Integer.valueOf(lexemeBuffer));
							}
							break;
					} 					
				}
		}
	}
}